﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;

namespace Borg
{
    public class Loader : RuntimeElement
    {
        private readonly object _lockObject = new object();
        private readonly Dictionary<AssemblyElement, Assembly> _loadedAssemblies =
            new Dictionary<AssemblyElement, Assembly>();

        protected internal Loader(Runtime runtime)
            : base(runtime)
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve;
        }

        private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var assemblyFile = FindAssemblyFile(new AssemblyName(args.Name));

            return
                assemblyFile != null
                ? Assembly.LoadFrom(assemblyFile)
                : null;
        }

        private Assembly OnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var assemblyFile = FindAssemblyFile(new AssemblyName(args.Name));
            
            return
                assemblyFile != null
                ? Assembly.ReflectionOnlyLoadFrom(assemblyFile)
                : Assembly.ReflectionOnlyLoad(args.Name);
        }

        private string FindAssemblyFile(AssemblyName assemblyName)
        {
            return (from path in Runtime.LoaderDirectories
                    from assemblyFile in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly)
                    where (assemblyFile.EndsWith(".dll") || assemblyFile.EndsWith(".exe")) &&
                          assemblyFile.Substring(0, assemblyFile.Length - ".dll".Length).EndsWith(assemblyName.Name)
                    let asmName = AssemblyName.GetAssemblyName(assemblyFile)
                    where asmName != null &&
                          assemblyName.Name.Equals(asmName.Name) &&
                          (!(assemblyName.Version != null) ||
                           assemblyName.Version.Equals(asmName.Version)) &&
                          (assemblyName.CultureInfo == null ||
                           assemblyName.CultureInfo.Equals(asmName.CultureInfo))
                    let publicKeyToken = assemblyName.GetPublicKeyToken()
                    where publicKeyToken == null ||
                          asmName.GetPublicKeyToken() != null &&
                          Equals(publicKeyToken, asmName.GetPublicKeyToken())
                    select assemblyFile).FirstOrDefault();
        }

        private static bool Equals(byte[] publicKeyToken1, byte[] publicKeyToken2)
        {
            return
                publicKeyToken1.Length == publicKeyToken2.Length &&
                !publicKeyToken1.Where((t, i) => t != publicKeyToken2[i]).Any();
        }

        public Assembly GetAssembly(MetaElement metaElement)
        {
            if (metaElement == null)
                throw new ArgumentNullException("metaElement");

            lock (Runtime)
                return InternalGetAssembly(metaElement);
        }

        public Type GetType(SlotDefinition slotDefinition)
        {
            if (slotDefinition == null)
                throw new ArgumentNullException("slotDefinition");

            lock (Runtime)
                return InternalGetType(slotDefinition);
        }

        public Type GetType(ParamDefinition paramDefinition)
        {
            if (paramDefinition == null)
                throw new ArgumentNullException("paramDefinition");

            lock (Runtime)
                return InternalGetType(paramDefinition);
        }

        public Type GetType(ExtensionType extensionType)
        {
            if (extensionType == null)
                throw new ArgumentNullException("extensionType");

            lock (Runtime)
                return InternalGetType(extensionType);
        }

        public bool IsLoaded(AssemblyElement assemblyElement)
        {
            if (assemblyElement == null)
                throw new ArgumentNullException("assemblyElement");

            lock (Runtime)
                return InternalIsLoaded(assemblyElement);
        }

        public bool ReflectionOnlyIsLoaded(AssemblyElement assemblyElement)
        {
            if (assemblyElement == null)
                throw new ArgumentNullException("assemblyElement");

            lock (Runtime)
                return InternalReflectionOnlyIsLoaded(assemblyElement);
        }

        public void Load(Contract contract)
        {
            if (contract == null)
                throw new ArgumentNullException("contract");

            lock (Runtime)
            {
                if (InternalIsLoaded(contract))
                    throw new InvalidOperationException(
                        string.Format(
                            "Contract {0}:'{1}' is already loaded",
                            contract.Id, contract.Name));
                
                InternalLoad(contract);
            }
        }

        public void Load(Plugin plugin)
        {
            if (plugin == null)
                throw new ArgumentNullException("plugin");

            lock (Runtime)
            {
                if (InternalIsLoaded(plugin))
                    throw new InvalidOperationException(
                        string.Format(
                            "Plugin {0}:'{1}' is already loaded",
                            plugin.Id, plugin.Name));
                
                InternalLoad(plugin);
            }
        }

        public void ReflectionOnlyLoad(Contract contract)
        {
            if (contract == null)
                throw new ArgumentNullException("contract");

            lock (Runtime)
            {
                if (InternalIsLoaded(contract))
                    throw new InvalidOperationException(
                        string.Format(
                            "Contract {0}:'{1}' is already loaded",
                            contract.Id, contract.Name));
                
                InternalReflectionOnlyLoad(contract);
            }
        }

        public void ReflectionOnlyLoad(Plugin plugin)
        {
            if (plugin == null)
                throw new ArgumentNullException("plugin");

            lock (Runtime)
            {
                if (InternalIsLoaded(plugin))
                    throw new InvalidOperationException(
                        string.Format(
                            "Plugin {0}:'{1}' is already loaded",
                            plugin.Id, plugin.Name));
                
                InternalReflectionOnlyLoad(plugin);
            }
        }

        protected internal virtual bool InternalIsLoaded(AssemblyElement assemblyElement)
        {
            return assemblyElement.AssemblyState == AssemblyState.Loaded;
        }

        protected internal virtual bool InternalReflectionOnlyIsLoaded(AssemblyElement assemblyElement)
        {
            return assemblyElement.AssemblyState == AssemblyState.ReflectionOnlyLoaded;
        }

        protected internal virtual Assembly InternalGetAssembly(AssemblyElement assemblyElement)
        {
            lock (_lockObject)
                return _loadedAssemblies[assemblyElement];
        }

        protected internal virtual Assembly InternalGetAssembly(MetaElement metaElement)
        {
            return InternalGetAssembly(metaElement.AssemblyElement);
        }

        protected internal virtual Type InternalGetType(Assembly assembly, string typeName)
        {
            return assembly.GetType(typeName);
        }

        protected internal virtual Type InternalGetType(SlotDefinition slotDefinition)
        {
            return InternalGetType(InternalGetAssembly(slotDefinition), slotDefinition.TypeName);
        }

        protected internal virtual Type InternalGetType(ParamDefinition paramDefinition)
        {
            return Type.GetType(paramDefinition.TypeName);
        }

        protected internal virtual Type InternalGetType(ExtensionType extensionType)
        {
            return InternalGetType(InternalGetAssembly(extensionType), extensionType.TypeName);
        }

        protected internal virtual Assembly InternalLoad(Contract contract)
        {
            var assembly = InternalLoad1(contract);

            InternalSetAssembly(contract, assembly);
            
            return assembly;
        }

        protected internal virtual Assembly InternalLoad(Plugin plugin)
        {
            var assembly = InternalLoad1(plugin);
            
            InternalSetAssembly(plugin, assembly);
            
            return assembly;
        }

        protected internal virtual Assembly InternalLoad1(AssemblyElement assemblyElement)
        {
            return InternalIsLoaded(assemblyElement)
                       ? Assembly.Load(assemblyElement.AssemblyName)
                       : (assemblyElement.AssemblyName != null
                              ? Assembly.Load(assemblyElement.AssemblyName)
                              : Assembly.LoadFrom(assemblyElement.AssemblyUri.AbsoluteUri));
        }

        protected internal virtual Assembly InternalReflectionOnlyLoad(Contract contract)
        {
            var assembly = InternalReflectionOnlyLoad1(contract);
            
            InternalSetAssembly(contract, assembly);
            
            return assembly;
        }

        protected internal virtual Assembly InternalReflectionOnlyLoad(Plugin plugin)
        {
            var assembly = InternalReflectionOnlyLoad1(plugin);
            
            InternalSetAssembly(plugin, assembly);
            
            return assembly;
        }

        protected internal virtual Assembly InternalReflectionOnlyLoad1(AssemblyElement assemblyElement)
        {
            return InternalIsLoaded(assemblyElement)
                       ? Assembly.Load(assemblyElement.AssemblyName)
                       : (InternalReflectionOnlyIsLoaded(assemblyElement)
                              ? Assembly.ReflectionOnlyLoad(assemblyElement.AssemblyName.FullName)
                              : (assemblyElement.AssemblyName != null
                                     ? Assembly.ReflectionOnlyLoad(assemblyElement.AssemblyName.FullName)
                                     : Assembly.ReflectionOnlyLoadFrom(assemblyElement.AssemblyUri.AbsoluteUri)));
        }

        protected internal virtual void InternalSetAssembly(Contract contract, Assembly assembly)
        {
            lock (_lockObject)
            {
                _loadedAssemblies[contract] = assembly;

                if (assembly == null)
                {
                    contract.AssemblyName = null;

                    if (contract.AssemblyState == AssemblyState.Discovered)
                        return;

                    contract.AssemblyState = AssemblyState.Unloaded;
                }
                else
                {
                    contract.AssemblyName = assembly.GetName();

                    contract.AssemblyState =
                        assembly.ReflectionOnly
                            ? AssemblyState.ReflectionOnlyLoaded
                            : AssemblyState.Loaded;
                }
            }
        }

        protected internal virtual void InternalSetAssembly(Plugin plugin, Assembly assembly)
        {
            lock (_lockObject)
            {
                _loadedAssemblies[plugin] = assembly;
                Thread.MemoryBarrier();
                
                if (assembly == null)
                {
                    plugin.AssemblyName = null;
                    
                    if (plugin.AssemblyState == AssemblyState.Discovered)
                        return;

                    plugin.AssemblyState = AssemblyState.Unloaded;
                }
                else
                {
                    if (assembly.ReflectionOnly)
                        plugin.AssemblyState = AssemblyState.ReflectionOnlyLoaded;
                    else
                    {
                        SetAppDomain(plugin);
                        plugin.AssemblyState = AssemblyState.Loaded;
                    }

                    plugin.AssemblyName = assembly.GetName();
                }
            }
        }

        private static void SetAppDomain(Plugin plugin)
        {
            if (plugin.AppDomain == null)
                plugin.AppDomain = AppDomain.CurrentDomain;

            else if (plugin.AppDomain != AppDomain.CurrentDomain)
                throw new InvalidOperationException();
        }
    }
}
